<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=835896
-->
<head>
  <meta charset="utf-8">
  <title>Test for Bug 835896</title>
  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
  <script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
  <script type="application/javascript;version=1.8">
window.onload = function() {
  const Cu = Components.utils;
  const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
  const promise = require("promise");
  const {InspectorFront} = require("devtools/shared/fronts/inspector");
  const {WalkerSearch, WalkerIndex} =
    require("devtools/server/actors/utils/walker-search");
  const {console} = Cu.import("resource://gre/modules/Console.jsm", {});

  SimpleTest.waitForExplicitFinish();

  let walkerActor = null;
  let walkerSearch = null;
  let inspectee = null;
  let inspector = null;

  // WalkerSearch specific tests.  This is to make sure search results are
  // coming back as expected.
  // See also test_inspector-search-front.html.

  addAsyncTest(function* setup() {
    info ("Setting up inspector and walker actors.");

    let url = document.getElementById("inspectorContent").href;

    yield new promise(resolve => {
      attachURL(url, function(err, client, tab, doc) {
        inspectee = doc;
        inspector = InspectorFront(client, tab);
        resolve();
      });
    });

    let walkerFront = yield inspector.getWalker();
    ok(walkerFront, "getWalker() should return an actor.");

    walkerActor = DebuggerServer._searchAllConnectionsForActor(walkerFront.actorID);
    ok(walkerActor,
      "Got a reference to the walker actor (" + walkerFront.actorID + ")");

    walkerSearch = walkerActor.walkerSearch;

    runNextTest();
  });

  addAsyncTest(function* testIndexExists() {
    info ("Testing basic index APIs exist.");

    let index = new WalkerIndex(walkerActor);
    ok(index.data.size > 0, "public index is filled after getting");

    index.clearIndex();
    ok(!index._data, "private index is empty after clearing");
    ok(index.data.size > 0, "public index is filled after getting");

    index.destroy();
    runNextTest();
  });

  addAsyncTest(function* testSearchExists() {
    info ("Testing basic search APIs exist.");

    ok(walkerSearch, "walker search exists on the WalkerActor");
    ok(walkerSearch.search, "walker search has `search` method");
    ok(walkerSearch.index, "walker search has `index` property");
    is(walkerSearch.walker, walkerActor, "referencing the correct WalkerActor");

    let search = new WalkerSearch(walkerActor);
    ok(search, "a new search instance can be created");
    ok(search.search, "new search instance has `search` method");
    ok(search.index, "new search instance has `index` property");
    isnot(search, walkerSearch, "new search instance differs from the WalkerActor's");

    search.destroy();
    runNextTest();
  });

  addAsyncTest(function* testEmptySearch() {
    info ("Testing search with an empty query.");
    results = walkerSearch.search("");
    is(results.length, 0, "No results when searching for ''");

    results = walkerSearch.search(null);
    is(results.length, 0, "No results when searching for null");

    results = walkerSearch.search(undefined);
    is(results.length, 0, "No results when searching for undefined");

    results = walkerSearch.search(10);
    is(results.length, 0, "No results when searching for 10");

    runNextTest();
  });

  addAsyncTest(function* testBasicSearchData() {
    let testData = [
    {
      desc: "Search for tag with one result.",
      search: "body",
      expected: [
        {node: inspectee.body, type: "tag"}
      ]
    },
    {
      desc: "Search for tag with multiple results",
      search: "h2",
      expected: [
        {node: inspectee.querySelectorAll("h2")[0], type: "tag"},
        {node: inspectee.querySelectorAll("h2")[1], type: "tag"},
        {node: inspectee.querySelectorAll("h2")[2], type: "tag"},
      ]
    },
    {
      desc: "Search for selector with multiple results",
      search: "body > h2",
      expected: [
        {node: inspectee.querySelectorAll("h2")[0], type: "selector"},
        {node: inspectee.querySelectorAll("h2")[1], type: "selector"},
        {node: inspectee.querySelectorAll("h2")[2], type: "selector"},
      ]
    },
    {
      desc: "Search for selector with multiple results",
      search: ":root h2",
      expected: [
        {node: inspectee.querySelectorAll("h2")[0], type: "selector"},
        {node: inspectee.querySelectorAll("h2")[1], type: "selector"},
        {node: inspectee.querySelectorAll("h2")[2], type: "selector"},
      ]
    },
    {
      desc: "Search for selector with multiple results",
      search: "* h2",
      expected: [
        {node: inspectee.querySelectorAll("h2")[0], type: "selector"},
        {node: inspectee.querySelectorAll("h2")[1], type: "selector"},
        {node: inspectee.querySelectorAll("h2")[2], type: "selector"},
      ]
    },
    {
      desc: "Search with multiple matches in a single tag expecting a single result",
      search: "💩",
      expected: [
        {node: inspectee.getElementById("💩"), type: "attributeValue"}
      ]
    },
    {
      desc: "Search that has tag and text results",
      search: "h1",
      expected: [
        {node: inspectee.querySelector("h1"), type: "tag"},
        {node: inspectee.querySelector("h1 + p").childNodes[0], type: "text"},
        {node: inspectee.querySelector("h1 + p > strong").childNodes[0], type: "text"},
      ]
    },
    ]

    for (let {desc, search, expected} of testData) {
      info("Running test: " + desc);
      let results = walkerSearch.search(search);
      isDeeply(results, expected,
        "Search returns correct results with '" + search + "'");
    }

    runNextTest();
  });

  addAsyncTest(function* testPseudoElements() {
    info ("Testing ::before and ::after element matching");

    let beforeElt = new _documentWalker(inspectee.querySelector("#pseudo"),
                                        inspectee.defaultView).firstChild();
    let afterElt = new _documentWalker(inspectee.querySelector("#pseudo"),
                                       inspectee.defaultView).lastChild();
    let styleText = inspectee.querySelector("style").childNodes[0];

    // ::before
    let results = walkerSearch.search("::before");
    isDeeply(results, [ {node: beforeElt, type: "tag"} ],
             "Tag search works for pseudo element");

    results = walkerSearch.search("_moz_generated_content_before");
    is(results.length, 0, "No results for anon tag name");

    results = walkerSearch.search("before element");
    isDeeply(results, [
      {node: styleText, type: "text"},
      {node: beforeElt, type: "text"}
    ], "Text search works for pseudo element");

    // ::after
    results = walkerSearch.search("::after");
    isDeeply(results, [ {node: afterElt, type: "tag"} ],
             "Tag search works for pseudo element");

    results = walkerSearch.search("_moz_generated_content_after");
    is(results.length, 0, "No results for anon tag name");

    results = walkerSearch.search("after element");
    isDeeply(results, [
      {node: styleText, type: "text"},
      {node: afterElt, type: "text"}
    ], "Text search works for pseudo element");

    runNextTest();
  });

  addAsyncTest(function* testSearchMutationChangeResults() {
    info ("Testing search before and after a mutation.");
    let expected = [
      {node: inspectee.querySelectorAll("h3")[0], type: "tag"},
      {node: inspectee.querySelectorAll("h3")[1], type: "tag"},
      {node: inspectee.querySelectorAll("h3")[2], type: "tag"},
    ];

    let results = walkerSearch.search("h3");
    isDeeply(results, expected, "Search works with tag results");

    yield mutateDocumentAndWaitForMutation(() => {
      expected[0].node.remove();
    });

    results = walkerSearch.search("h3");
    isDeeply(results, [
      expected[1],
      expected[2]
    ], "Results are updated after removal");

    yield new promise(resolve => {
      info("Waiting for a mutation to happen");
      let observer = new inspectee.defaultView.MutationObserver(() => {
        resolve();
      });
      observer.observe(inspectee, {attributes: true, subtree: true});
      inspectee.body.setAttribute("h3", "true");
    });

    results = walkerSearch.search("h3");
    isDeeply(results, [
      {node: inspectee.body, type: "attributeName"},
      expected[1],
      expected[2]
    ], "Results are updated after addition");

    yield new promise(resolve => {
      info("Waiting for a mutation to happen");
      let observer = new inspectee.defaultView.MutationObserver(() => {
        resolve();
      });
      observer.observe(inspectee, {attributes: true, childList: true, subtree: true});
      inspectee.body.removeAttribute("h3");
      expected[1].node.remove();
      expected[2].node.remove();
    });

    results = walkerSearch.search("h3");
    is(results.length, 0, "Results are updated after removal");

    runNextTest();
  });

  runNextTest();

  function mutateDocumentAndWaitForMutation(mutationFn) {
    return new promise(resolve => {
      info("Listening to markup mutation on the inspectee");
      let observer = new inspectee.defaultView.MutationObserver(resolve);
      observer.observe(inspectee, {childList: true, subtree: true});
      mutationFn();
    });
  }
};
  </script>
</head>
<body>
<a id="inspectorContent" target="_blank" href="inspector-search-data.html">Test Document</a>
<p id="display"></p>
<div id="content" style="display: none">

</div>
<pre id="test">
</pre>
</body>
</html>
